React 前端工程教學講座 - 使用 JSON server 模擬 RESTful API 打造電影平台 API 應用
2022-12-13 Tue
此篇為到大學教導第二週的實作範例的教導內容,這次實作範例包含使用 MUI 製作頁面後渲染頁面並透過 useState 和 useEffect 與 axios 發送至 TMDB 電影查詢 API 以及使用 JSON server 模擬 RESTful API 的 CRUD。
雙週課程大綱為以下內容
1.React 是什麼 2.Component 基本介紹 3.常用 Hook 介紹 4.JSX 介紹 5.React router 6.串接第三方 API 7.使用 JSON Server 模擬 RestfulAPI
希望對於 React 有興趣的人可以參考,若內容有問題也可以告訴我🙂
註冊 TMDB
一個可以查詢電影 API 的網站
記得收信
MUI
基本安裝
內容包含 emotion 樣式和 emotion for react 以及 mui 的核心
MUI 預設基本內建預設樣式使用 emotion
npm install @mui/material @emotion/react @emotion/styledMUI ICON 安裝
以下指令為安裝 MUI 的 icon 其內容就是安裝 MUI 的 icon
npm install @mui/icons-materialreact-router-dom
安裝 react router dom 其內容是讓 URL 與 UI 同步,換句話說就是輸入某個網址列將會渲染某個畫面。
npm install react-router-domAxios
一個用來發送 API 的套件,可以更有效的管理 API
npm install axiosSASS
作為 CSS 預處理器
npm install sassjson-server
作為暫時伺服器用
npm install -g json-server 建立一個.env 檔案
敏感資料不該上傳置 github
新增一個.env 檔案
REACT_APP_API_KEY=輸入你的api key在 react-create-app 可以設定環境變數方法請參考官網 react-create-app 環境變數
其他 vs code extension 推薦
auto import
auto close tag
鍵入一個 tag 的時候自動生成另一個對應的 tag,詳情參見官方網站
Auto Rename Tag
Auto Rename Tag
當重新命名 tag 的時候可以自動重新命名對應的 tag,詳情請見官方網站
AutoFileName
import 的時候能夠提示路徑 AutoFileName
EMMET
🙂開始專案了🙂
reset css
建立一個 reset css
css module
- 樣式的作用域只會在該 component
- 解決 CSS 全域汙染
★建立 Footer
Footer 樣式
.footer {
text-align: center;
padding: 20px 0;
background-color: #1976d2;
color: white;
font-size: 24px;
}Footer component
import React from 'react'
import styles from "./footer.module.scss";
const Footer = () => {
const { footer } = styles;
//透過解構將其取出,在 className 的部分改帶入變數
return (
<footer className={footer}>Footer</footer>
)
}
export default Footerscss(sass)-CSS 預處理器
- 編譯後產生純 css
- 巢狀選擇器寫法,避免 css 選擇器需重複撰寫
其他更多用法參見官方網站 scss 官方網站
★建立 Header
Header 樣式
.header {
text-align: center;
padding: 20px 0;
background-color: #1976d2;
color: white;
ul {
display: flex;
justify-content: flex-end;
li {
a {
color: white;
font-size: 24px;
padding: 20px;
&:hover {
opacity: 0.8;
}
}
}
}
}
Header component
Link 是 react-router-dom 的元件,有點像是 a tag
import React from 'react'
import styles from './header.module.scss'
import { Link } from "react-router-dom"
const Header = () => {
const { header } = styles;
return (
<header className={header}>
<ul>
<li><Link to="/">首頁</Link></li>
<li><Link to="/favorite">我的最愛</Link></li>
</ul>
</header >
)
}
export default HeaderMUI—sx props, color
sx prop
- sx 是 css 的超集
- 所有 MUI 的 component 都可以使用的 props,用來添加 css 樣式在 component 上
- 別名寫法例如
- m→margin
- pt→padding-top
- bgcolor→backgroundColor
更多可以參考官網 MUI-sx prop
MUI-color
MUI 的 color 調色盤
如下圖

使用方式如下
import { red } from '@mui/material/colors';
const color = red[500]; //回傳#f44336更多可以參考MUI-color
★建立 Content
Content Component
import { Container, Box } from "@mui/material"
import { lightBlue } from '@mui/material/colors';
export default function Content({ children }) {
return (
<Box sx={{ bgcolor: lightBlue[50], p: 6 }}>
<Container>
{children}
</Container>
</Box>
);
}react-router-dom
建置網站的時候,根據不同的網址,擁有不同的頁面,我們稱之為 Router。
使用方式建立一個 router 的 js 檔案,取名叫做 index.js
原先已經建立的頁面檔需要引入作為參數帶入
createBrowserRouter
帶入一個陣列形式區分不同的 router,其內容為一個物件。
以下為物件屬性和說明
- path 路由網址
- element 要回傳的 component(或稱頁面檔)
- loader 當載入的頁面需要事先請求資料的時候可以帶入一個 function 作為請求
- errorElement 當遇到錯誤的時候要回傳的 component
實際 code 可能如下
//請求資料的 function
async function fetchPopular() {
const config = {
method: "get",
url: "https://api.themoviedb.org/3/movie/popular?api_key=你的 APIKEY",
};
return await axios(config);
} catch (error) {
console.log(error);
return [];
}
}
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,//回傳 HomePage
loader: fetchPopular,//請求 funciton
errorElement: <NotFound />,//錯誤時候要回傳的 component
},
//多個路由將以第二個物件回傳
{
path: "/favorite",
element: <FavoritePage />,
},
]);
★建立 router 的 index.js
router 的 index.js
import { createBrowserRouter } from "react-router-dom";
import HomePage from "../pages/HomePage.jsx";
import FavoritePage from "../pages/FavoritePage.jsx";
import NotFound from "../pages/NotFound.jsx";
import axios from "axios";
async function fetchPopular() {
try {
const config = {
method: "get",
url: "https://api.themoviedb.org/3/movie/popular?api_key=你的 APIKey",
};
return await axios(config);
} catch (error) {
console.log(error);
return [];
}
}
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
loader: fetchPopular,
errorElement: <NotFound />,
},
{
path: "/favorite",
element: <FavoritePage />,
},
]);
export default router;
★更動 index.js
index.js
在 index.js 當中加入 RouterProvider 應當可以如期渲染出對應的東西
其程式碼如下
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import router from "./router";
import "./reset.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<RouterProvider router={router}>
<App />
</RouterProvider>
</React.StrictMode>
);建立 HomePage 頁面
本頁重點
- 使用 MUI Grid 格線系統
- react route useLoaderData
- 透過 props 將剛剛提取的資料傳遞到 MovieCard
- js 知識點 map 的 callback funciton 也可以解構
react route useLoaderData
這個 hook 用來提取剛剛 loader 所發送的 API 請求 更多用法請參考React-Router-useLoaderData
Grid 請參考
參考鐵人賽文章 格線系統、基本用法、Responsive values、Grid version 2?—MUI
★建立 HomePage 實際程式碼如下
map 的 callback function 也可以解構
提取到的資料透過 prop 傳遞
import React from 'react'
import { Grid } from "@mui/material"
import { useLoaderData } from "react-router-dom";
import Content from '../components/Content/Content'
import Header from '../components/Header/Header'
import Footer from '../components/Footer/Footer'
import MovieCard from '../components/MovieCard/MovieCard'
const HomePage = () => {
const { data: { results: popularMovies } = {} } = useLoaderData();
return (
<>
<Header>
</Header>
<Content >
<Grid container spacing={2} sx={{ justifyContent: "center" }}>
{
popularMovies.map(({ overview, title, backdrop_path }, index) => (
<Grid item xs={12} lg={4} xl={3} md={6} sm={6} key={index}>
<MovieCard overview={overview} title={title} backdrop_path={backdrop_path} />
</Grid>
))
}
</Grid>
</Content>
<Footer />
</>
)
}
export default HomePage建立 MovieCardComponent
★建立 MovieCard
本小節小重點
- 將得到的 props 透過解構
- js 知識點,使用邏輯運算子||作為渲染內容
import React from 'react'
import { Card, CardContent, CardMedia, Typography } from "@mui/material"
const MovieCard = ({ overview, title, backdrop_path }) => {
return (
<Card key={title} sx={{ maxWidth: 345, height: '100%' }}>
<CardMedia
component="img"
image={`https://image.tmdb.org/t/p/w400${backdrop_path}`}
alt={title}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{title}
</Typography>
<Typography variant="body2" color="text.secondary">
{overview || "暫無說明"}
</Typography>
</CardContent>
</Card>
)
}
export default MovieCardNotFoundPage 頁面
useNavigate 作為跳轉
- 可以使用程式碼的方式實現頁面導航
- 可以帶入第二個參數傳遞 state
- 與 history statck 一樣可以帶入 -1 表示上一頁
useRouteError();
由於先前有使用 errorElement,因此可以使用 useRouteError 錯誤訊息
★建立 NotFoundPage
NotFoundPage
下面的功能使用 useEffect 撰寫,當兩秒鐘後 navigate 進行畫面跳轉
import React, { useEffect } from 'react';
import { Typography } from "@mui/material"
import { useRouteError, useNavigate } from "react-router-dom";
import Content from '../components/Content/Content'
import Header from '../components/Header/Header'
import Footer from '../components/Footer/Footer'
const NotFound = () => {
const error = useRouteError();
const navigate = useNavigate();
useEffect(() => {
const timer = setTimeout(() => {
navigate("/");
}, 2000);
return () => clearTimeout(timer);
}, [navigate]);
return (
<>
<Header />
<Content >
<Typography variant="h1" sx={{ textAlign: "center" }} component="h2" >
{error.statusText}
</Typography>
</Content>
<Footer />
</>
)
}
export default NotFoundinstanceAPI 檔案
- 封裝 API 更好管理
- 透過命名來知道是哪個 instance 實體
★建立 instanceAPI
建立 axios 的實體
由於需要帶的參數固定,因此可以使用 create 的方式建立實體
更多可以參考The Axios Instance-doc
import axios from "axios";
export const instance = axios.create({
baseURL: "http://localhost:3004",
headers: {
"Content-Type": "application/json",
},
});
使用 json-server
可以用的方法如下
GET /posts GET /posts/1 POST /posts PUT /posts/1 PATCH /posts/1 DELETE /posts/1
更多用法可以參考 json-server npm 如下 json-serverDoc
npm install -g json-server 建立一個 db.json
{
"favorites": [
{
"id": 1670875867685,
"title": "哈利波特",
"finished": false
}
]
}指令輸入以下指令即可開啟 3004 的伺服器
json-server --watch db.json --port 3004使用 PostMan 測試 CURD
點擊 + 符號 新增 collection

可以點擊 add request

新建完 request 後記得 ctrl+s 儲存
可以興建以下 request

🙂最後一個頁面了🙂
建立 FavoritePage 頁面
推薦看鐵人賽文章
本章節小重點
- 當使用 Post 或者 update 或者 delete 完後再次 get 一次資料,建議使用 async await 可以確保資料已經新增到資料庫後後再次 get
- onClick 如果需要帶入變數的話要一個 callback function,因為 onClick 是 function 不是 function 執行的結果
- 使用 form 表單元素搭配 onSubmit 可以作為 enter 送出訊號,記得使用 e.preventDefault() 預防畫面閃一下
★建立 FavoritePage 頁面
import React, { useEffect, useState } from 'react'
import { List, ListItem, IconButton, ListItemText, TextField, Box } from '@mui/material';
import { TaskAlt as TaskAltIcon, Delete as DeleteIcon } from '@mui/icons-material'
import Header from '../components/Header/Header'
import Footer from '../components/Footer/Footer'
import Content from '../components/Content/Content'
import { instance } from "../API/instanceAPI"
const FavoritePage = () => {
const [favorite, setFavorite] = useState([]);
const [inputFavorite, setInputFavorite] = useState('');
useEffect(() => {
(async () => {
try {
const { data } = await instance.get('/favorites')
setFavorite(data)
} catch (error) {
console.log(error);
}
})();
}, [])
const newFavoriteHandler = (e) => {
e.preventDefault();
(async () => {
try {
const posetResponse = await instance.post('/favorites', {
id: Date.now(),
"title": inputFavorite,
"finished": false
})
console.log(posetResponse);
const { data } = await instance.get('/favorites')
setFavorite(data)
} catch (error) {
console.log(error);
}
})();
instance.get('/favorites')
.then(response => {
const { data } = response
setFavorite(data)
}).catch(error => {
console.log(error);
})
setInputFavorite('');
}
const finishedHandler = (id, title, finished) => {
(async () => {
try {
const putResponse = await instance.put(`/favorites/${id}`, {
id,
title,
finished: !finished
})
console.log(putResponse);
const { data } = await instance.get('/favorites')
setFavorite(data)
} catch (error) {
console.log(error);
}
})();
}
const deleteHandler = (id) => {
(async () => {
try {
const deleteResponse = await instance.delete(`/favorites/${id}`)
console.log(deleteResponse);
const { data } = await instance.get('/favorites')
setFavorite(data)
} catch (error) {
console.log(error);
}
})();
}
return (
<>
<Header />
<Content>
<Box sx={{ boxShadow: 1, maxWidth: "600px", margin: "0 auto", p: 6, }}>
<Box onSubmit={newFavoriteHandler} component="form" noValidate
autoComplete="off">
<TextField
value={inputFavorite}
fullWidth={true}
label="輸入待看清單"
variant="filled"
onChange={
(e) => { setInputFavorite(e.target.value) }
}
/>
</Box>
<List>
{favorite.map(({ title, id, finished }) => {
return (<ListItem key={id}>
<ListItemText
primary={title}
sx={{ textDecoration: finished ? "line-through" : "none" }}
/>
<IconButton
sx={{ m: 1 }}
edge="end"
aria-label="TaskAltIcon"
onClick={() => (finishedHandler(id, title, finished))}>
<TaskAltIcon />
</IconButton>
<IconButton
sx={{ m: 1 }}
edge="end"
aria-label="delete"
onClick={() => (deleteHandler(id))}>
<DeleteIcon />
</IconButton>
</ListItem>)
})}
</List>
</Box>
</Content>
<Footer />
</>
)
}
export default FavoritePage最後如下圖

另外一個頁面

完成🎉🥳🎊